import datetime
import inspect
import os
import platform
import sys
import time
import traceback
from contextvars import ContextVar

import aioboto3
import aiomysql
import psutil
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from re_common.baselibrary.utils.basedir import BaseDir
from re_common.baselibrary.utils.basefile import BaseFile
from re_common.baselibrary.utils.baseip import BaseIp
from re_common.baselibrary.utils.core.requests_core import MsgCode
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import PlainTextResponse
from starlette.staticfiles import StaticFiles
from tinydb import TinyDB

from app_include_router import set_include_router
from apps.core.g_decorator import repeat_task
from apps.sql_app.mmongodb import MongoDBClient, connect_to_mongo, close_mongo_connection, Coll
from apps.sql_app.redis import get_redis_pool
from settings import get_settings
import asyncio
sys.setrecursionlimit(100)

app = FastAPI(title="caiji project", openapi_url=f"/api/v1/openapi.json")
# app.add_middleware(GZipMiddleware, minimum_size=1000)
app.state.run_model = "uvicorn"
app.state.ip = str(BaseIp().GetLocalIPByPrefix("192.168"))
app.mount("/mdoc/static", StaticFiles(directory="static/docs"), name="static")

request_ctx_var = ContextVar("request", default=None)

# 设置apirouter的挂载
set_include_router(app)

sets = get_settings()


@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    # 设置会话全局变量 相当于flask的g
    request.state.try_his = []
    request_ctx_var.set(request)
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response


@app.exception_handler(Exception)
async def all_exception_handler(request: Request, exc: Exception):
    """
    全局所有异常
    :param request:
    :param exc:
    :return:
    """
    from apps.file_uploadfile.models import ServerErrModel
    # print(request.__dict__)
    # request.scope包含很多信息
    m = ServerErrModel().dict()
    m["ip_port"] = app.state.ip + ":" + str(app.state.port)
    m["client_ip"] = str(request.client.host)
    m["write_time"] = str(datetime.datetime.now())
    m["request_url"] = str(request.url)
    # https://github.com/tiangolo/fastapi/issues/1216 作者目前还没有解决这个bug
    # m["request_body"] = await request.body()
    m["request_body"] = str(request.state.mbody)
    m["err_type"] = str(sys.exc_info()[0])
    m["err_msg"] = str(sys.exc_info()[1])
    m["traceback_str"] = traceback.format_exc()
    from apps.crawler_platform.util.sqlhelper import format_t
    m["stack"] = ''.join([format_t(inspect.getframeinfo(i.frame)) for i in inspect.stack()[:10]])
    save_dicts = ServerErrModel.parse_obj(m)
    m_result = Coll.get_server_exception()

    result = await m_result.insert_one(save_dicts.dict(by_alias=True))
    return PlainTextResponse(str(exc), status_code=MsgCode.SERVER_ERROR)


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    """
    参数与给定的model不一致导致的错误
    :param request:
    :param exc:
    :return:
    """
    # body = await request.body()
    # body = body.decode()
    # print(body)
    return PlainTextResponse(str(exc), status_code=400)


# app.mount("/static", StaticFiles(directory="static"), name="static")

# uvicorn main:app --reload --host=0.0.0.0 --port=8000


def set_port_tag(app):
    """
    当启动模式为两个进程时，通过父进程id获取端口与标签
    :param app:
    :return:
    """
    if app.state.run_model == "uvicorn":
        ppid = os.getppid()
        pid = os.getpid()
        if os.path.exists(str(ppid) + ".db") and os.path.isfile(str(ppid) + ".db"):
            t_db = TinyDB(str(ppid) + ".db")
            pppid = ppid
        else:
            t_db = TinyDB(str(pid) + ".db")
            pppid = pid

        for item in t_db:
            if str(pppid) + "_port" in item.keys():
                app.state.port = item[str(pppid) + "_port"]
            if str(pppid) + "_tags" in item.keys():
                app.state.tags = item[str(pppid) + "_tags"]
        # 清理上面产生的文件
        dirs = BaseDir.get_file_dir_absolute(__file__)
        for path, dirNames, fileNames in os.walk(dirs, topdown=True):
            for filename in fileNames:
                lists = filename.split(".")
                if len(lists) == 2 and lists[0].isdigit() and lists[1] == "db":
                    try:
                        psutil.Process(int(lists[0]))
                    except psutil.NoSuchProcess:
                        filepath = os.path.join(path, filename)
                        if BaseFile.is_file_exists(filepath):
                            try:
                                BaseFile.remove_file(filepath)
                            except:
                                print("{}文件已被删除，不需要再次删除".format(filename))


@app.on_event("startup")
async def startup():
    """
    在所有 startup 事件处理程序完成 之前，您的应用程序将不会开始接收请求 。
    :return:
    """
    sys.setrecursionlimit(500)
    MongoDBClient.dbjsonhtmlsdb = await connect_to_mongo(
        sets.MONGODB_URI_HTMLSBD,
        sets.MONGODB_URI_HTMLSBD_DB)
    MongoDBClient.dbjsonhtmllatestsdb = await connect_to_mongo(
        sets.MONGODB_URI_HTMLSBDLATEST,
        sets.MONGODB_URI_HTMLSBDLATEST_DB)
    MongoDBClient.dbjsonoversealatestsdb = await connect_to_mongo(
        sets.MONGODB_URI_OVERSEA_SBDLATEST,
        sets.MONGODB_URI_HOVERSEA_SBDLATEST_DB)
    if "Windows" != platform.system():
        if BaseIp().GetLocalIPByPrefix("192.168") in sets.SDB_USE_IP:
            from re_common.baselibrary.tools.sequoiadb_utils import SequoiadbUtils
            sh = SequoiadbUtils()
            MongoDBClient.sdb_jsonhtml_conn = sh.set_db_data(eval(sets.SDB_CONN)).set_list_hosts(
                eval(sets.SDB_LISTS)).build()

    from apps.sql_app.database import proxys_sql_conn
    # from apps.sql_app.database import down_product_conn
    await proxys_sql_conn.connect()
    # await down_product_conn.connect()
    app.state.redis = await get_redis_pool()
    # 重复 设置 app.state.port计数，3次，保证port正确
    app.state.port_pid_retry_num = 0
    app.state.pool = await aiomysql.create_pool(host=sets.MYSQL_HOST_1,
                                                port=sets.MYSQL_PORT_1,
                                                user=sets.MYSQL_USER_1,
                                                password=sets.MYSQL_PWD_1,
                                                db=sets.MYSQL_DATEBASE_1,
                                                autocommit=True)
    app.state.aioboto3 = aioboto3.Session(aws_access_key_id=sets.AWS_LOCAL_ACCESS_KEY_ID,
                               aws_secret_access_key=sets.AWS_LOCAL_SECRET_ACCESS_KEY)
    # boto3 = BaseBoto3(aws_access_key_id=sets.AWS_LOCAL_ACCESS_KEY_ID,
    #           aws_secret_access_key=sets.AWS_LOCAL_SECRET_ACCESS_KEY,
    #           endpoint_url=sets.AWS_LOCAL_ENDPOINT_URL)
    # boto3.conn_session()
    # boto3.set_is_low_level(True)
    # boto3.get_client()
    # app.state.boto3 = boto3
    set_port_tag(app)
    print('启动 *********')
    from apps.allsubdb.proxies_control.proxy_class import ProxyClass
    proxy_cls = ProxyClass()
    await proxy_cls.start_proxy_task()


# 周期性任务
# @app.on_event("startup")
# @repeat_every(seconds=20, raise_exceptions=True)
@app.on_event('startup')
@repeat_task(seconds=20, wait_first=True)
async def periodic():
    if app.state.port_pid_retry_num < 3:
        app.state.port_pid_retry_num = app.state.port_pid_retry_num + 1
        set_port_tag(app)

    if not hasattr(app.state, "port") or not isinstance(app.state.port, int):
        print("app没有 port 属性")
        return False
    ip_port = app.state.ip + ":" + str(app.state.port)
    version = sets.VERSION
    tags = app.state.tags

    try:
        from pip._internal.commands.show import search_packages_info as search_packages_info
        package_name = 're-common'
        # here I use next, because search_packages_info returns a generator
        package_info = next(search_packages_info([package_name]))
        re_common_version = package_info.version
    except:
        re_common_version = ""

    sql = "insert into server_address(ip_port,count,tags,version,re_common_version) values ('%s',1,'%s','%s','%s') on duplicate key update count = count +1,tags = '%s',version = '%s',re_common_version='%s'" % (
        ip_port, tags, version, re_common_version, tags, version, re_common_version)
    from apps.crawler_platform.util.sqlhelper import SQLHelper
    await SQLHelper.execute_commit(sql)


@app.on_event("shutdown")
async def shutdown():
    """
    要添加应在应用程序关闭时运行的功能，请使用事件声明它 "shutdown"
    :return:
    """

    await close_mongo_connection(MongoDBClient.dbjsonhtmlsdb)
    await close_mongo_connection(MongoDBClient.dbjsonhtmllatestsdb)
    from apps.sql_app.database import proxys_sql_conn
    # from apps.sql_app.database import down_product_conn
    await proxys_sql_conn.disconnect()
    # await down_product_conn.disconnect()
    await app.state.redis.close()
    # await app.state.redis_163_7.close()
    app.state.pool.close()
    await app.state.pool.wait_closed()
    print('关闭 *********')
    from apps.allsubdb.proxies_control.proxy_class import ProxyClass
    proxy_cls = ProxyClass()
    await proxy_cls.end_proxy_task()
    if "Windows" != platform.system():
        if BaseIp().GetLocalIPByPrefix("192.168") in sets.SDB_USE_IP:
            MongoDBClient.sdb_jsonhtml_conn.close()
